﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Windows.Forms;
using ComponentFactory.Krypton.Toolkit;
using StableDiffusionTools.Animation;
using StableDiffusionTools.Interop;
using StableDiffusionTools.Utils;

namespace StableDiffusionTools.Controls
{
    public enum ImageSizeMode
    {
        Zoom,
        Fit
    }
    
    public enum ImageInterpolationMode
    {
        HighQualityBicubic,//Specifies high-quality, bicubic interpolation. Prefiltering is performed to ensure high-quality shrinking. This mode produces the highest quality transformed images.
        HighQualityBilinear,//Specifies high-quality, bilinear interpolation. Prefiltering is performed to ensure high-quality shrinking.
        Bicubic, //Specifies bicubic interpolation. No prefiltering is done. This mode is not suitable for shrinking an image below 25 percent of its original size.
        Bilinear,//Specifies bilinear interpolation. No prefiltering is done. This mode is not suitable for shrinking an image below 50 percent of its original size.
        Default,//Specifies default mode.
        High,//Specifies high quality interpolation.
        Low,//Specifies low quality interpolation.
        NearestNeighbor,//Specifies nearest-neighbor interpolation.
        StretchBlt//Fastest
    }

    public class ImageControl : Control
    {
        private PointF startPoint;

        IntPtr hImageDC = IntPtr.Zero;
        IntPtr[] hBitmaps = null;
        System.Threading.Timer animationTimer;
        object animationLocker = new object();
        // private IntPtr hOldBitmap = IntPtr.Zero;
        bool clonedImage = false;
        double lastZoom = 1.0f;
       // VScrollBar vScroll;
        //HScrollBar hScroll;

        //Public Properties
        public int FrameCount { get => Frames?.Length ?? 0; }
        public bool IsAnimation { get => FrameCount > 1; }
        public Frame[] Frames { get; private set; }
        public bool AutoPlay { get; set; } = true;

        private double zoom = 1.0;
        public double Zoom
        {
            get => zoom;
            set
            {
                if (zoom == value) return;

                double lastZoom = zoom;
                zoom = value;

                PointF diff = default(PointF);
                if (sizeMode != ImageSizeMode.Zoom)
                {
                    sizeMode = ImageSizeMode.Zoom;
                }
                else
                {
                    double zoomDiff = lastZoom - zoom;
                    diff = new PointF((float)((ClientSize.Width * zoomDiff) / lastZoom / 2.0), (float)((ClientSize.Height * zoomDiff) / lastZoom / 2.0));
                }

                UpdateImageOrigin(diff);
            }
        }

        private bool resetZoomOnImageLoad = false;
        public bool ResetZoomOnImageLoad
        {
            get => resetZoomOnImageLoad;
            set
            {
                if (value == resetZoomOnImageLoad) return;

                resetZoomOnImageLoad = value;
            }
        }

        private int frameIndex = 0;
        public int FrameIndex
        {
            get => frameIndex;
            set
            {
                if (frameIndex == value) return;
                if (value < FrameCount)
                {
                    frameIndex = value;
                    //Gdi32.SelectObject(hImageDC, hBitmaps[frameIndex]);
                    Invalidate();
                }
            }
        }

        private int animationFPS = 30;
        public int AnimationFPS
        {
            get => animationFPS;
            set
            {
                if (animationFPS == value) return;
                if (AnimationUtil.IsValidFPS(value))
                {
                    animationFPS = value;

                    ChangeTimer();
                }
            }
        }

        private bool isAnimating = false;
        public bool IsAnimating
        {
            get => isAnimating;
            set
            {
                if (isAnimating == value) return;
                isAnimating = value;

                ChangeTimer();
            }
        }

        private RectangleF sourceRect;
        public RectangleF SourceRect
        {
            get => sourceRect;
            set
            {
                if (sourceRect == value) return;

                sourceRect = value;
                UpdateImageOrigin();
            }
        }

        private RectangleF destinationRect;
        public RectangleF DestinationRect
        {
            get => destinationRect;
            set
            {
                if (destinationRect == value) return;

                destinationRect = value;
                UpdateImageOrigin();
            }
        }

        private CompositingQuality compositingQuality = CompositingQuality.HighQuality;
        public CompositingQuality CompositingQuality
        {
            get => compositingQuality;
            set
            {
                if (value == compositingQuality) return;

                compositingQuality = value;
                Invalidate();
            }
        }

        private PixelOffsetMode pixelOffsetMode = PixelOffsetMode.HighQuality;
        public PixelOffsetMode PixelOffsetMode
        {
            get => pixelOffsetMode;
            set
            {
                if (value == pixelOffsetMode) return;

                pixelOffsetMode = value;
                Invalidate();
            }
        }
        private SmoothingMode smoothingMode = SmoothingMode.HighQuality;
        public SmoothingMode SmoothingMode
        {
            get => smoothingMode;
            set
            {
                if (value == smoothingMode) return;

                smoothingMode = value;
                Invalidate();
            }
        }

        private ImageInterpolationMode interpolationMode = ImageInterpolationMode.StretchBlt;
        public ImageInterpolationMode InterpolationMode 
        {
            get => interpolationMode;
            set
            {
                if (value == interpolationMode) return;

                interpolationMode = value;
                Invalidate();
            } 
        }

        private ImageSizeMode sizeMode = ImageSizeMode.Zoom;
        public ImageSizeMode SizeMode
        {
            get => sizeMode;
            set
            {
                if (sizeMode == value) return;

                sizeMode = value;
                UpdateImageOrigin(forceInvalidate: true);
            }
        }

        private Bitmap image;
        public Bitmap Image
        {
            get => image;
            set
            {
                SetImage(value);
            }
        }

        public Size ImageSize
        {
            get;
            protected set;
        } = default(Size);

        private IPalette _palette;

        //Constructor
        public ImageControl()
        {
            SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
            SetStyle(ControlStyles.Selectable, true);

            //SuspendLayout();

            hImageDC = ImageUtil.CreateCompatibleDC(this);
            animationTimer = new System.Threading.Timer(AnimationTick, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
            

            //vScroll = new VScrollBar();
            ////vScroll.Anchor = (((AnchorStyles.Top | AnchorStyles.Bottom) | AnchorStyles.Right));
            //vScroll.Dock = DockStyle.Right;
            //vScroll.Size = new Size(vScroll.Width, Bottom - vScroll.Width);
            ////vScroll.Location = new Point(Width - vScroll.Width, 0);
            //Controls.Add(vScroll);

            //hScroll = new HScrollBar();
            ////hScroll.Anchor = (((AnchorStyles.Left | AnchorStyles.Right) | AnchorStyles.Bottom));
            //hScroll.Dock = DockStyle.Bottom;
            //hScroll.Size = new Size(Right - hScroll.Height, hScroll.Height);
            ////hScroll.Location = new Point(0, Height - hScroll.Height);
            //Controls.Add(hScroll);

            //ResumeLayout(false);

            _palette = KryptonManager.CurrentGlobalPalette;
            if (_palette != null)
                _palette.PalettePaint += new EventHandler<PaletteLayoutEventArgs>(OnPalettePaint);

            KryptonManager.GlobalPaletteChanged += new EventHandler(OnGlobalPaletteChanged);
            BackColor = _palette?.GetBackColor1(PaletteBackStyle.ControlClient, PaletteState.Normal) ?? BackColor;
        }

        private void OnPalettePaint(object sender, PaletteLayoutEventArgs e)
        {
            Invalidate();
        }

        private void OnGlobalPaletteChanged(object sender, EventArgs e)
        {
            if (_palette != null)
                _palette.PalettePaint -= new EventHandler<PaletteLayoutEventArgs>(OnPalettePaint);

            _palette = KryptonManager.CurrentGlobalPalette;

            BackColor = _palette?.GetBackColor1(PaletteBackStyle.ControlClient, PaletteState.Normal) ?? BackColor;

            if (_palette != null)
                _palette.PalettePaint += new EventHandler<PaletteLayoutEventArgs>(OnPalettePaint);

            Invalidate();
        }

        #region IDisposable
        ~ImageControl()//Finalizer
        {
            Dispose(false);
        }

        protected override void Dispose(bool disposing)
        {
            //Unmanaged
            DeleteGDIBitmaps();
            DeleteMemoryDC();

            //Managed
            if (disposing)
            {
                DisposeFrames();
                DisposeClonedImage();
                animationTimer.Dispose();

                // (10) Unhook from the palette events
                if (_palette != null)
                {
                    _palette.PalettePaint -= new System.EventHandler<ComponentFactory.Krypton.Toolkit.PaletteLayoutEventArgs>(OnPalettePaint);
                    _palette = null;
                }

                // (11) Unhook from the static events, otherwise we cannot be garbage collected
                ComponentFactory.Krypton.Toolkit.KryptonManager.GlobalPaletteChanged -= new System.EventHandler(OnGlobalPaletteChanged);
            }
            base.Dispose(disposing);
        }

        public new void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion

        //Public Methods
        public virtual void SetImage(Bitmap value, bool forceRefresh = false)
        {
            lock (animationLocker)
            {
                bool wasAnimating = isAnimating;
                IsAnimating = false;

                DeleteGDIBitmaps();
                DisposeFrames();
                DisposeClonedImage();

                if (value == null)
                {
                    image = null;
                    ImageSize = Size.Empty;
                    Invalidate();
                    return;
                }

                if (value.PixelFormat != PixelFormat.Format32bppPArgb) //Optimize bitmap for rendering
                {
                    image = value.Clone(new Rectangle(0, 0, value.Width, value.Height), PixelFormat.Format32bppPArgb);
                    clonedImage = true;
                }
                else
                {
                    image = value;
                }

                ImageSize = image.Size;

                frameIndex = 0;
                Frames = AnimationUtil.GetFrames(image);
                hBitmaps = AnimationUtil.GetHBitmaps(Frames);

                IsAnimating = IsAnimation && (wasAnimating || AutoPlay);

                if(resetZoomOnImageLoad)
                    zoom = 1.0f;

                ResetOrigin(forceInvalidate: !forceRefresh);

                if (forceRefresh)
                    Refresh();
                //Load
            }
        }

        public void ResetZoom()
        {
            Zoom = 1.0f;
        }

        public void ResetOrigin(bool forceInvalidate = false)
        {
            destinationRect = new RectangleF();
            sourceRect = new RectangleF();
            UpdateImageOrigin(forceInvalidate: forceInvalidate);
        }

        public void Reset(bool forceInvalidate = false)
        {
            zoom = 1.0f;
            ResetOrigin(forceInvalidate: forceInvalidate);
        }

        public void Fit()
        {
            SizeMode = ImageSizeMode.Fit;
        }

        public void ActualSize()
        {
            zoom = 1.0f;
            sizeMode = ImageSizeMode.Zoom;
            UpdateImageOrigin();
        }

        public void ToggleAnimating()
        {
            if (IsAnimation)
                IsAnimating = !IsAnimating;
        }

        //Protected Override
        protected override void OnPaint(PaintEventArgs e)
        {
            Graphics g = e.Graphics;

            PaintImage(g);
            DrawOverlay(e);
            PaintZoom(g);
           //base.OnPaint(e);
        }

        private void PaintZoom(Graphics g)
        {
            string zString = string.Format("{0:0}%", zoom * 100f);
            SizeF sSize = g.MeasureString(zString, Font);
            g.FillRectangle(Brushes.LightGray, 0, 0, sSize.Width, sSize.Height);
            g.DrawString(zString, Font, Brushes.Black, 0, 0);
        }

        protected virtual void DrawOverlay(PaintEventArgs e)
        {

        }

        protected override void OnSizeChanged(EventArgs e)
        {
            UpdateImageOrigin();
        }

        protected override void OnMouseDown(MouseEventArgs e)
        {
            base.OnMouseDown(e);
            if (e.Button == MouseButtons.Left)
                Focus();

            if (FrameCount == 0 || sizeMode == ImageSizeMode.Fit)
                return;

            if (e.Button == MouseButtons.Left)
                startPoint = MousePosition;
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);
            if (FrameCount == 0 || sizeMode == ImageSizeMode.Fit)
                return;

            if (e.Button == MouseButtons.Left)
            {
                PointF deltaP = new PointF(MousePosition.X - startPoint.X, MousePosition.Y - startPoint.Y);
                UpdateImageOrigin(deltaP);
                startPoint = MousePosition;
            }
        }

        protected override void OnMouseWheel(MouseEventArgs e)
        {
            base.OnMouseWheel(e);

            if (!((HandledMouseEventArgs)e).Handled)
            {
                int delta = e.Delta / 120;
                int deltaAbs = Math.Abs(delta);
                double newZoom = zoom;
                double zoomFactor = delta < 0 ? 0.9f : 1.1f;

                for (int i = 0; i < deltaAbs; i++)
                    newZoom = MathUtil.Clamp(newZoom * zoomFactor, 0.01f, 10.0f);

                if (newZoom > 0.9f && newZoom < 1.1f)
                    newZoom = 1.0f;

                Zoom = (float)Math.Round(newZoom, 3);
            }
        }

        protected override void OnKeyDown(KeyEventArgs e)
        {
            base.OnKeyDown(e);

            if(e.KeyCode == Keys.Space)
            {
                ToggleAnimating();
            }
            else if(e.KeyCode == Keys.Left)
            {
                IsAnimating = false;
                DecrementFrame();
            }
            else if (e.KeyCode == Keys.Right)
            {
                IsAnimating = false;
                IncrementFrame();
            }
        }

        protected override void OnPreviewKeyDown(PreviewKeyDownEventArgs e)
        {
            if(e.KeyCode == Keys.Left || e.KeyCode == Keys.Right)
            {
                e.IsInputKey = true;
            }
            base.OnPreviewKeyDown(e);
        }

        //Private Methods
        private void PaintImage(Graphics g)
        {
            if (FrameCount == 0)
                return;

            if (sizeMode == ImageSizeMode.Zoom)
            {
                Rectangle dest = new Rectangle((int)Math.Floor(destinationRect.X), (int)Math.Floor(destinationRect.Y), (int)Math.Floor(destinationRect.Width), (int)Math.Floor(destinationRect.Height));
                Rectangle source = new Rectangle((int)Math.Floor(sourceRect.X), (int)Math.Floor(sourceRect.Y), (int)Math.Floor(sourceRect.Width), (int)Math.Floor(sourceRect.Height));
                if (interpolationMode == ImageInterpolationMode.StretchBlt)
                {
                    bool selectBitmap = false;
                    IntPtr old = IntPtr.Zero;
                    if (hBitmaps != null)
                    {
                        old = Gdi32.SelectObject(hImageDC, hBitmaps[FrameIndex]);
                        selectBitmap = true;
                    }

                    IntPtr pTarget = g.GetHdc();
                    if(zoom == 1.0f)
                    {
                        Gdi32.BitBlt(pTarget, dest.X, dest.Y, dest.Width, dest.Height, hImageDC, source.X, source.Y, TernaryRasterOperations.SRCCOPY);
                    }
                    else
                    {
                        Gdi32.SetStretchBltMode(pTarget, StretchMode.COLORONCOLOR);
                        Gdi32.StretchBlt(pTarget, dest.X, dest.Y, dest.Width, dest.Height, hImageDC, source.X, source.Y, source.Width, source.Height, TernaryRasterOperations.SRCCOPY);
                    }
                    g.ReleaseHdc(pTarget);

                    if (selectBitmap)
                        Gdi32.SelectObject(hImageDC, old);
                }
                else
                {
                    g.InterpolationMode = GetInterpolationMode(interpolationMode);
                    g.CompositingQuality = compositingQuality;
                    g.PixelOffsetMode = pixelOffsetMode;
                    g.SmoothingMode = smoothingMode;
                    ImageAttributes attr = new ImageAttributes();
                    attr.SetWrapMode(WrapMode.TileFlipXY);
                    g.DrawImage(Frames[frameIndex].Image, dest, sourceRect.X, sourceRect.Y, sourceRect.Width, sourceRect.Height, GraphicsUnit.Pixel, attr);
                }
            }
            else if (sizeMode == ImageSizeMode.Fit)
            {
                bool selectBitmap = false;
                IntPtr old = IntPtr.Zero;
                if (hBitmaps != null)
                {
                    old = Gdi32.SelectObject(hImageDC, hBitmaps[FrameIndex]);
                    selectBitmap = true;
                }

                if (interpolationMode == ImageInterpolationMode.StretchBlt)
                {
                    IntPtr pTarget = g.GetHdc();
                    Gdi32.SetStretchBltMode(pTarget, StretchMode.COLORONCOLOR);
                    Gdi32.StretchBlt(pTarget, (int)destinationRect.X, (int)destinationRect.Y, (int)destinationRect.Width, (int)destinationRect.Height, hImageDC, 0, 0, Frames[frameIndex].Width, Frames[frameIndex].Height, TernaryRasterOperations.SRCCOPY);
                    g.ReleaseHdc(pTarget);

                    if (selectBitmap)
                        Gdi32.SelectObject(hImageDC, old);
                }
                else
                {
                    g.InterpolationMode = GetInterpolationMode(interpolationMode);
                    g.CompositingQuality = compositingQuality;
                    g.PixelOffsetMode = pixelOffsetMode;
                    g.SmoothingMode = smoothingMode;
                    ImageAttributes attr = new ImageAttributes();
                    attr.SetWrapMode(WrapMode.TileFlipXY);
                    g.DrawImage(Frames[frameIndex].Image, new Rectangle((int)destinationRect.X, (int)destinationRect.Y, (int)destinationRect.Width, (int)destinationRect.Height), 0, 0, Frames[frameIndex].Width, Frames[frameIndex].Height, GraphicsUnit.Pixel, attr);
                }
            }
        }

        protected virtual void UpdateImageOrigin(PointF deltaP = default(PointF), bool forceInvalidate = false)
        {
            bool UpdateDest = false;
            bool UpdateSource = false;
            bool UpdateZoom = lastZoom != zoom;

            if (FrameCount > 0)
            {
                Image img = Frames[frameIndex].Image;
                RectangleF NewDestinationRect = default(RectangleF);
                RectangleF NewSourceRect = default(RectangleF);
                if (sizeMode == ImageSizeMode.Zoom)
                {
                    RectangleF source = new RectangleF(sourceRect.X, sourceRect.Y, ClientSize.Width, ClientSize.Height);
                    RectangleF dest = new RectangleF(destinationRect.X, destinationRect.Y, ClientSize.Width, ClientSize.Height);
                    //Centre X
                    double widthZoomed = img.Width * zoom;
                    if (widthZoomed < ClientSize.Width)
                    {
                        dest.X = (float)((ClientSize.Width - widthZoomed) / 2.0);
                        dest.Width = (float)widthZoomed;
                        source.X = 0;
                        source.Width = img.Width;
                    }
                    else //BoundCheck X
                    {
                        dest.X = 0;
                        source.Width = (float)(ClientSize.Width / zoom);
                        source.X = (float)MathUtil.Clamp(source.X - (deltaP.X / zoom), 0, img.Width - (ClientSize.Width / zoom));
                    }
                    //Centre Y
                    double heightZoomed = img.Height * zoom;
                    if (heightZoomed < ClientSize.Height)
                    {
                        dest.Y = (float)((ClientSize.Height - heightZoomed) / 2.0);
                        dest.Height = (float)heightZoomed;
                        source.Y = 0;
                        source.Height = img.Height;
                    }
                    else //BoundCheck Y
                    {
                        dest.Y = 0;
                        source.Height = (float)(ClientSize.Height / zoom);
                        source.Y = (float)MathUtil.Clamp(source.Y - (deltaP.Y / zoom), 0, img.Height - (ClientSize.Height / zoom));
                    }
                    NewSourceRect = source;
                    NewDestinationRect = dest;

                }
                else if (sizeMode == ImageSizeMode.Fit)
                {
                    zoom = ImageUtil.CalculateAspectRatio(img.Size, ClientSize);
                    PointF destLoc = new PointF((float)((ClientSize.Width - (img.Width * zoom)) / 2.0), (float)((ClientSize.Height - (img.Height * zoom)) / 2.0));
                    SizeF destSize = new SizeF((float)(img.Width * zoom), (float)(img.Height * zoom));

                    NewSourceRect = new RectangleF(0, 0, img.Width, img.Height);
                    NewDestinationRect = new RectangleF(destLoc, destSize);
                }
                if(destinationRect != NewDestinationRect)
                {
                    UpdateDest = true;
                    destinationRect = NewDestinationRect;
                }
                if(sourceRect != NewSourceRect)
                {
                    UpdateSource = true;
                    sourceRect = NewSourceRect;
                }
            }

            lastZoom = zoom;
            if (UpdateDest || UpdateSource || UpdateZoom || forceInvalidate)
                Invalidate();
        }

        private InterpolationMode GetInterpolationMode(ImageInterpolationMode interpolationMode)
        {
            InterpolationMode result;
            switch (interpolationMode)
            {
                case ImageInterpolationMode.Bicubic:
                result = System.Drawing.Drawing2D.InterpolationMode.Bicubic;
                break;
                case ImageInterpolationMode.Bilinear:
                result = System.Drawing.Drawing2D.InterpolationMode.Bilinear;
                break;
                case ImageInterpolationMode.High:
                result = System.Drawing.Drawing2D.InterpolationMode.High;
                break;
                case ImageInterpolationMode.HighQualityBicubic:
                result = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
                break;
                case ImageInterpolationMode.HighQualityBilinear:
                result = System.Drawing.Drawing2D.InterpolationMode.HighQualityBilinear;
                break;
                case ImageInterpolationMode.Low:
                result = System.Drawing.Drawing2D.InterpolationMode.Low;
                break;
                case ImageInterpolationMode.NearestNeighbor:
                result = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
                break;
                case ImageInterpolationMode.Default:
                default:
                result = System.Drawing.Drawing2D.InterpolationMode.Default;
                break;
            }
            return result;
        }

        private void AnimationTick(object o)
        {
            IncrementFrame();
        }

        private void IncrementFrame()
        {
            lock (animationLocker)
            {
                if (IsAnimation)
                {
                    FrameIndex = GetNextFrameIndex();
                }
            }
        }

        private void DecrementFrame()
        {
            lock (animationLocker)
            {
                if (IsAnimation)
                {
                    FrameIndex = GetPreviousFrameIndex();
                }
            }
        }

        private int GetNextFrameIndex()
        {//TODO: handle FrameCount = 0
            return (frameIndex + 1) % FrameCount;
        }

        private int GetPreviousFrameIndex()
        {//TODO: handle FrameCount = 0
            int newFrameIndex = frameIndex - 1;
            if (newFrameIndex < 0)
            {
                if (FrameCount > 0)
                    newFrameIndex = FrameCount - 1;
                else
                    newFrameIndex = 0;
            }
            return newFrameIndex;
        }

        private void ChangeTimer()
        {
            int period = AnimationUtil.GetPeriodFromFPS(animationFPS);
            int dueTime = isAnimating ? period : System.Threading.Timeout.Infinite;
            animationTimer.Change(dueTime, period);
        }

        private void DeleteGDIBitmaps()
        {
            if (hBitmaps == null) return;

            for (int i = 0; i < hBitmaps.Length; i++)
                Gdi32.DeleteObject(hBitmaps[i]);
            hBitmaps = null;
        }

        private void DisposeFrames()
        {
            if (Frames == null) return;


            for (int i = Frames.Length - 1; i >= 0; i--)
                Frames[i].Dispose();
            Frames = null;
        }

        private void DisposeClonedImage()
        {
            if (!clonedImage) return;

            image?.Dispose();
            clonedImage = false;
        }

        private void DeleteMemoryDC()
        {
            if (hImageDC == IntPtr.Zero) return;

            Gdi32.DeleteDC(hImageDC);
            hImageDC = IntPtr.Zero;
        }
    }
}
